////////////////////////////////////////////////////////////////////////////////
//
//  FLEX USER GROUP HOKURIKU
//  Copyright 2009 Flex User Group Hokuriku
//  All Rights Reserved.
// 
//  NOTICE: Flex User Group Hokuriku permits you to use, 
//  modify, and distribute this file 
//  in accordance with the terms of the license agreement accompanying it.
//
////////////////////////////////////////////////////////////////////////////////
package net.fxug.hokuriku.chocolat.service
{
    import flash.errors.IllegalOperationError;
    import flash.events.Event;
    import flash.events.EventDispatcher;
    import flash.events.IOErrorEvent;
    import flash.events.SecurityErrorEvent;
    import flash.events.TimerEvent;
    import flash.net.Socket;
    import flash.utils.Timer;

    import mx.logging.ILogger;

    import net.fxug.hokuriku.chocolat.events.SocketEvent;
    import net.fxug.hokuriku.chocolat.events.SonarEvent;
    import net.fxug.hokuriku.chocolat.events.VoteEvent;
    import net.fxug.hokuriku.chocolat.model.NetworkInterfaceData;
    import net.fxug.hokuriku.chocolat.service.data.SocketData;
    import net.fxug.hokuriku.chocolat.service.data.SocketServerData;
    import net.fxug.hokuriku.chocolat.utils.LoggingUtil;

    /**
     * スピーカー側にTCP接続成功時にディスパッチされる。
     * Voteするたびに切断する仕様なので頻繁に投げられる。うざいなら、一度受け取ったら、接続が切れるまでLISTENしないのが吉
     *
     * @eventType net.fxug.hokuriku.chocolat.events.SocketEvent
     */
    [Event(name="ServerConnectSuccess", type="net.fxug.hokuriku.chocolat.events.SocketEvent")]

    /**
     * スピーカーが見つけられなかった
     * スピーカーが存在しないか、ネットワーク的に接続不可の状態なのかどちらか
     *
     * @eventType net.fxug.hokuriku.chocolat.events.SocketEvent
     */
    [Event(name="ServerConnectFailed", type="net.fxug.hokuriku.chocolat.events.SocketEvent")]

    /**
     * スピーカーに接続できなくなった
     * 接続できていたスピーカーにメッセージ送信もしくは接続確認した際に接続不能だった場合
     *
     * @eventType net.fxug.hokuriku.chocolat.events.SocketEvent
     */
    [Event(name="ServerConnectClose", type="net.fxug.hokuriku.chocolat.events.SocketEvent")]


    /**
     * ソケット接続クライアントクラス
     *
     * 1.UDPでブロードキャストを利用してスピーカーを探査
     * 2.スピーカーを見つけたらTCPで通信
     *
     * TODO Windows7同士の接続だと挙動がおかしいので調査
     * TODO IPv4のネットワーク情報が2つ以上見つかった場合はユーザーに選択してもらう必要ありそうだ
     * TODO 接続できなかった場合の処理、接続が切れた場合の処理
     *
     * @author mac
     */
    public class SocketClient extends EventDispatcher
    {
        private static var logger:ILogger = LoggingUtil.getLogger(SocketClient);

        public static const CHECK_INTERVAL:int = 60 * 1000; // 定期的に接続確認するタイマーの間隔

        // スピーカー探査用クラス。DatagramSocketクラスを用いてLAN環境内のスピーカーを探索する
        private var sonar:Sonar;

        // スピーカー接続用のソケットクラス
        private var socket:Socket;

        // 自分自身のネットワーク情報
        private var networkData:NetworkInterfaceData;

        // 接続先のスピーカー情報
        private var serverData:SocketServerData;

        // 送信するメッセージデータ
        private var unsentMessage:XML = new XML();

        private var timer:Timer = new Timer(CHECK_INTERVAL);

        /**
         * コンストラクタ
         *
         * @param inNetworkData
         * @throws IllegalOperationError
         */
        public function SocketClient(inNetworkData:NetworkInterfaceData)
        {
            if (inNetworkData is NetworkInterfaceData)
            {
                this.networkData = inNetworkData;
            }
            else
            {
                throw new IllegalOperationError("Illegal Constructor param error");
            }
        }

        /**
         * IP指定でスピーカー探査
         */
        public function connectByAddress(address:String):void
        {
            logger.info("SocketClient::connectByAddress");

            this.ping(address);
        }

        /**
         * ブロードキャストを用いてスピーカー探査
         */
        public function connectByBroadcast():void
        {
            logger.info("SocketClient::connectByBroadcast");

            this.ping(this.networkData.broadcast);
        }

        /**
         * スピーカー探査
         */
        private function ping(address:String):void
        {
            logger.info("SocketClient::Ping for {0}", address);

            if (!this.sonar)
            {
                logger.info("SocketClient:: new Sonar address" + networkData.address);
                this.sonar = new Sonar(this.networkData.address);
                this.sonar.addEventListener(SonarEvent.SERVER_REPLY_RECIEVED, serverReplyRecievedEventHandler);
                this.sonar.addEventListener(SonarEvent.SERVER_NOT_DISCOVER, serverNotDiscoveredEventHandler);
            }

            try
            {
                this.sonar.ping(address);
            }
            catch (e:Error)
            {
                logger.error("SocketClient::Ping catch error message = " + e.message);
                throw new Error(e.message);
            }
        }

        /**
         * ソナーがスピーカーからの返答を受け取ったイベントハンドラ
         *
         */
        private function serverReplyRecievedEventHandler(event:SonarEvent):void
        {
            logger.debug("SocketClient::serverReplyRecievedEventHandler");

            this.serverData = event.serverData;

            this.connectServer();
        }

        private function connectServer():void
        {
            if (this.socket && this.socket.connected)
            {
                this.socket.close();
            }

            logger.info("Create Socket port = {0} / address = {1}", SocketServer.PORT, this.serverData.address);

            this.socket = new Socket();

            this.socket.addEventListener(Event.CLOSE, socketCloseHandler);
            this.socket.addEventListener(Event.CONNECT, socketConnectEventHandler);
            this.socket.addEventListener(IOErrorEvent.IO_ERROR, socketConnectIOErrorEventHandler);
            this.socket.addEventListener(SecurityErrorEvent.SECURITY_ERROR, socketConnectIOErrorEventHandler);

            this.socket.connect(this.serverData.address, SocketServer.PORT);
        }

        public function destroy():void
        {
            logger.info("SocketClient::destroy");

            if (this.sonar)
            {
                this.sonar.removeEventListener(SonarEvent.SERVER_REPLY_RECIEVED, serverReplyRecievedEventHandler);
                this.sonar.removeEventListener(SonarEvent.SERVER_NOT_DISCOVER, serverNotDiscoveredEventHandler);
                this.sonar.dispose();
                this.sonar = null;
            }

            if (this.socket)
            {
                this.socket.removeEventListener(Event.CLOSE, socketCloseHandler);
                this.socket.removeEventListener(Event.CONNECT, socketConnectEventHandler);
                this.socket.removeEventListener(IOErrorEvent.IO_ERROR, socketConnectIOErrorEventHandler);
                this.socket.removeEventListener(SecurityErrorEvent.SECURITY_ERROR, socketConnectIOErrorEventHandler);

                if (this.socket.connected)
                {
                    this.socket.close();
                    this.socket = null;
                }
            }
        }

        /**
         * メッセージを送信する
         *
         * @param message メッセージ
         * @param vote goodEvent/badEvent
         * @param name 発言者名
         */
        public function sendMessage(message:String, vote:String, name:String = ""):void
        {
            this.unsentMessage = SocketData.getSocketDataForMessage(message, vote, name)

            if (this.socket && this.socket.connected)
            {
                logger.debug("発言=" + unsentMessage.toString());

                this.socket.writeUTFBytes(unsentMessage.toXMLString());
                this.socket.flush();
                this.socket.close();

                this.unsentMessage = null;
            }
            else
            {
                this.connectServer();
            }
        }


        private function socketConnectEventHandler(event:Event):void
        {
            logger.debug("SocketClient::socketConnectEventHandler");
            logger.debug("    スピーカーに接続成功!!");

            var socketEvent:SocketEvent = new SocketEvent(SocketEvent.SERVER_CONNECT_SUCCESS);
            socketEvent.networkData = this.networkData;
            this.dispatchEvent(socketEvent);

            if (this.unsentMessage)
            {
                this.socket.writeUTFBytes(unsentMessage.toXMLString());
                this.socket.flush();
                this.socket.close();
            }
            this.unsentMessage = null;

            if (!this.timer.running)
            {
                logger.debug("    生存確認タイマー開始");

                // 定期的に接続を確認するタイマーをスタート
                this.timer.addEventListener(TimerEvent.TIMER, checkServerAliveHandler);
                this.timer.start();
            }
        }

        private function checkServerAliveHandler(event:TimerEvent):void
        {
            logger.debug("SocketClient::checkServerAliveHandler");
            logger.info("    スピーカー生存確認を開始");

            this.connectServer();
        }

        /**
         * スピーカーを見つけることができなかった
         */
        private function serverNotDiscoveredEventHandler(event:SonarEvent):void
        {
            logger.debug("SocketClient::serverNotDiscoveredHandler");
            logger.info("    スピーカーを見つけることができませんでした");

            var socketEvent:SocketEvent = new SocketEvent(SocketEvent.SERVER_CONNECT_FAILED);
            this.networkData = this.networkData;

            this.dispatchEvent(socketEvent);
        }

        /**
         * 接続エラー(一度接続したスピーカーなどが終了などの理由で接続できなかった場合に発生)
         */
        private function socketConnectIOErrorEventHandler(event:IOErrorEvent):void
        {
            logger.error("SocketClient::socketConnectIOErrorEventHandler");

            var socketEvent:SocketEvent = new SocketEvent(SocketEvent.SERVER_CONNECT_CLOSE);
            this.dispatchEvent(socketEvent);

            if (this.timer)
            {
                logger.info("     タイーマー停止");
                this.timer.stop();
            }
        }

        private function socketCloseHandler(event:Event):void
        {
            logger.debug("SocketClient::socketCloseHandler");

            this.destroy();
        }

        private function socketSecurityErrorEventHandler(event:SecurityErrorEvent):void
        {
            logger.debug("SocketClient::socketConnectIOErrorEventHandler");
            this.destroy();
            throw new Error("SocketClient::securityError");
        }
    }
}

